package com.such.kit.database.mybatis;

import java.sql.Connection;
import java.util.Map;
import java.util.Properties;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import com.such.kit.Logger;
import com.such.kit.database.MetaDataCommons;
import com.such.kit.database.dialect.DialectCommons;
import com.such.kit.string.StringCommons;
import com.such.kit.validate.ValidateCommons;

/**
 * @author SUCH
 * <pre>
 * MyBatis 分页拦截器
 * 适用 MyBatis 版本 3.4.6+
 * 
 * 拦截器属性列表
 *   dbType 数据库类型，未指定则通过 {@link Connection} 获取
 *   statementSuffix statementId 匹配后缀，以指定后缀结尾的 statementId 才会拦截 [默认值为 ByPage]
 *   pageIndex 当前页 [默认值为 1] [下标为 1]
 *   pageIndexKey 当前页在参数列表中的 key [默认值为 pageIndex]
 *   pageLimit 当前页数据量 [默认值为 20]
 *   pageLimitKey 当前页数据量在参数列表中的 key [默认值为 pageLimit]
 * 
 * Intercepts 标注为拦截器
 * Signature 拦截器签名
 * type：拦截对象类型 [{@link Executor} | {@link ResultSetHandler} | {@link ParameterHandler} | {@link StatementHandler}]
 * method：指定对象类型中需要拦截的方法
 * args：指定拦截方法的入参列表
 * </pre>
 */
@Intercepts({
	@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class MybatisPagingInterceptor implements Interceptor {

	private String dbType;
	private String statementSuffix;
	private int pageIndex;
	private String pageIndexKey;
	private int pageLimit;	
	private String pageLimitKey;

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		// 获取 RoutingStatementHandler 并转换为接口 StatementHandler（在注解中拦截器指定了拦截的类型为 StatementHandler）
		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
		// 获取 StatementHandler 元信息
		MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
		// 分离代理对象链
		while(metaObject.hasGetter("h")) {
			Object obj = metaObject.getValue("h");
			metaObject = SystemMetaObject.forObject(obj);
		}
		while(metaObject.hasGetter("target")) {
			Object obj = metaObject.getValue("target");
			metaObject = SystemMetaObject.forObject(obj);
		}
		// 获取查询接口映射的相关信息
		MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
		String statementId = mappedStatement.getId();
		// 拦截以 statementSuffix 结尾的请求
		if (statementId.endsWith(this.statementSuffix)) {
			Logger.debug(getClass(), "MyBatis 分页拦截器包装语句.开始");
			Logger.debug(getClass(), StringCommons.merger("拦截到 statement [", statementId, "]"));
			Object[] args = invocation.getArgs();
			if (ValidateCommons.isEmpty(this.dbType)) {
				Connection conn = (Connection) args[0];
				this.dbType = MetaDataCommons.getDbType(conn, false);
				Logger.debug(getClass(), "从 Connection 获取数据库类型");
			}
			// 获取请求参数
			ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
			Map<String, Object> params = (Map<String, Object>) parameterHandler.getParameterObject();
			// 获取 sql
			String sql = (String) metaObject.getValue("delegate.boundSql.sql");
			/*
			 * 也可以通过 BoundSql 获取参数、sql，但是 BoundSql 并未提供 setSql 方法
			 * BoundSql boundSql = statementHandler.getBoundSql();
			 * Map<String, Object> params = boundSql.getParameterObject();
			 * String sql = boundSql.getSql();
			 */
			int pageIndex = this.pageIndex;
			int pageLimit = this.pageLimit;
			Object pageIndexObj = params.get(this.pageIndexKey);
			Object pageLimitObj = params.get(this.pageLimitKey);
			if (ValidateCommons.isNotEmpty(pageIndexObj)) {
				pageIndex = (int) pageIndexObj;
			}
			if (ValidateCommons.isNotEmpty(pageLimitObj)) {
				pageLimit = (int) pageLimitObj;
			}
			String pagingSql = DialectCommons.getInstance(this.dbType).SQL.buildPagingSql(sql, pageIndex, pageLimit);
			// 将构建完成的分页 sql 放回 MetaObject
			metaObject.setValue("delegate.boundSql.sql", pagingSql);
			Logger.debug(getClass(), StringCommons.merger("拦截器 properties [",
					"dbType: ", dbType, " | ",
					"statementSuffix: ", statementSuffix, " | ",
					"pageIndex: ", pageIndex, " | ",
					"pageIndexKey: ", pageIndexKey, " | ",
					"pageLimit: ", pageLimit, " | ",
					"pageLimitKey: ", pageLimitKey,
			"]"));
			Logger.debug(getClass(), StringCommons.merger("语句包装前 [", sql, "]"));
			Logger.debug(getClass(), StringCommons.merger("语句包装后 [", pagingSql, "]"));
			Logger.debug(getClass(), "MyBatis 分页拦截器包装语句.结束");
		}
		// 进入责任链的下一级
		return invocation.proceed();
	}

	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
        this.dbType = properties.getProperty("dbType");
        this.statementSuffix = properties.getProperty("statementSuffix", "ForPage");
        this.pageIndex = Integer.valueOf(properties.getProperty("pageIndex", "1"));
        this.pageIndexKey = properties.getProperty("pageIndexKey", "pageIndex");
        this.pageLimit = Integer.valueOf(properties.getProperty("pageLimit", "20"));
        this.pageLimitKey = properties.getProperty("pageLimitKey", "pageLimit");
	}

}
